package org.rambadger.command.model;

import org.axonframework.commandhandling.annotation.CommandHandler;
import org.axonframework.eventhandling.annotation.EventHandler;
import org.axonframework.eventsourcing.annotation.AbstractAnnotatedAggregateRoot;
import org.axonframework.eventsourcing.annotation.AggregateIdentifier;
import org.axonframework.eventsourcing.annotation.EventSourcedMember;
import org.rambadger.command.command.*;
import org.rambadger.command.event.*;

import java.util.*;

import static org.rambadger.command.command.AddUploadedFile.AddAssignmentFile;
import static org.rambadger.command.command.DeleteUploadedFile.DeleteAssignmentFile;
import static org.rambadger.command.event.FileDeleted.AssignmentFileDeleted;
import static org.rambadger.command.event.FileUploaded.AssignmentFileUploaded;

/**
 * Aggregate Root for Platforms.
 */
public class PlatformAggregate extends AbstractAnnotatedAggregateRoot<String> {
    private static final long serialVersionUID = 2268270505677763577L;

    @AggregateIdentifier
    private String platformId;
    private String platformName;
    private String description;

    @EventSourcedMember
    private Map<String, TrackEntity> tracks = new HashMap<>();
    @EventSourcedMember
    private Map<String, AssignmentEntity> assignments = new HashMap<>();

    PlatformAggregate() {}

    /**
     * Command handler that creates an new platform
     *
     * @param cmd the command
     */
    @CommandHandler
    public PlatformAggregate(CreatePlatform cmd) {
        apply(new PlatformCreated(cmd.getPlatformId(), cmd.getPlatformName(), cmd.getDescription()));
    }

    /**
     * Handles {@link TrackEntity track} creation.
     *
     * @param cmd the command
     */
    @CommandHandler
    public void createTrack(CreateTrack cmd) {
        //The there must not already be a track with that ID on the platform
        if (hasTrack(cmd.getTrackId())) {
            throw new RuntimeException(String.format("Track '%s' already exists for platform '%s'.", cmd.getTrackId(), platformId));
        }
        apply(new TrackCreated(cmd.getPlatformId(), cmd.getTrackId(), cmd.getTrackName(), cmd.getDescription()));
    }

    /**
     * Handle {@link AssignmentEntity assignment} creation.
     *
     * @param cmd the command
     */
    @CommandHandler
    public void createAssignment(CreateAssignment cmd) {
        //The there must not already be an assignment with that ID on the platform
        if (hasAssignment(cmd.getAssignmentId())) {
            throw new RuntimeException(String.format("Assignment '%s' already exists for platform '%s'.", cmd.getAssignmentId(), platformId));
        }
        //Create the assignment
        apply(new AssignmentCreated(cmd.getPlatformId(), cmd.getAssignmentId(), cmd.getAssignmentName(), cmd.getDescription(), cmd.getSubmissionPoints(), cmd.getReviewPoints()));

        if (cmd.getAssociatedTrack() != null) {
            //A track was specified in the command
            if (hasTrack(cmd.getAssociatedTrack())) {
                //The track exists, so add it to the end
                apply(new AssignmentAddedToTrack(cmd.getPlatformId(), cmd.getAssignmentId(), cmd.getAssignmentName(), tracks.get(cmd.getAssociatedTrack()).getAssignments().size()));
            } else {
                //Oops, the track doesn't exist
                //NOTE: This might be changed to simply ignore the issue and leave the assignment trackless
                throw new RuntimeException(String.format("Associated Track '%s' does not exist.", cmd.getAssociatedTrack()));
            }
        }
    }

    /**
     * Handles attaching files to assignments
     * @param cmd the command
     */
    @CommandHandler
    public void addAssignmentFile(AddAssignmentFile cmd) {
        AssignmentEntity assignment = getAssignment(cmd.getAssignmentId());
        if (assignment == null) {
            //Assignment must exist
            throw new RuntimeException(String.format("Assignment '%s/%s' does not exist.", platformId, cmd.getAssignmentId()));
        } else if (assignment.hasAttachment(cmd.getFilename())) {
            //Duplicate filenames are not allowed
            throw new RuntimeException(String.format("Assignment '%s/%s' already has a file named '%s'.", platformId, cmd.getAssignmentId(), cmd.getFilename()));
        } else {
            apply(new AssignmentFileUploaded(platformId, cmd.getAssignmentId(), cmd.getFilename(), cmd.getFileId()));
        }

    }

    /**
     * Remove an attached file from an assignment
     * @param cmd the command
     */
    @CommandHandler
    public void deleteSubmissionFile(DeleteAssignmentFile cmd) {
        AssignmentEntity assignment = getAssignment(cmd.getAssignmentId());
        if (assignment == null) {
            //Assignment must exist
            throw new RuntimeException(String.format("Assignment '%s/%s' does not exist.", platformId, cmd.getAssignmentId()));
        } else if (!assignment.hasAttachment(cmd.getFilename())) {
            //File must be present on the assignment
            throw new RuntimeException(String.format("Assignment '%s/%s' does not have a file named '%s'.", platformId, cmd.getAssignmentId(), cmd.getFilename()));
        } else {
            apply(new AssignmentFileDeleted(platformId, cmd.getAssignmentId(), cmd.getFilename()));
        }
    }

    /**
     * Re-assigns/re-orders a list of assignments to the specified track.
     *
     * @param cmd the command
     */
    @CommandHandler
    public void setTrackAssignments(SetTrackAssignments cmd) {
        String trackId = cmd.getTrackId();
        TrackEntity track = getTrack(trackId);
        if (track == null) {
            throw new RuntimeException(String.format("Track '%s.%s' does not exist.", platformId, trackId));
        }

        //Remove all assignments currently in the track that are not in the new set.
        Set<String> removedAssignments = new HashSet<>(track.getAssignments());
        removedAssignments.removeAll(cmd.getAssignments());

        for (String removedAssignment : removedAssignments) {
            apply(new AssignmentRemovedFromTrack(platformId, removedAssignment, trackId));
        }

        //This is effectively using a selection sort to re-order
        List<String> newAssignments = cmd.getAssignments();
        for (int index = 0; index < newAssignments.size(); index++) {
            String assignmentId = newAssignments.get(index);
            //Make sure the assignment actually exists
            if (!hasAssignment(assignmentId)) {
                throw new RuntimeException(String.format("Assignment '%s.%s' does not exist.", platformId, assignmentId));
            }

            AssignmentEntity assignment = getAssignment(assignmentId);
            //If the assignment is currently in a different track, throw an error
            if (!assignment.isFreeOrInTrack(trackId)) {
                throw new RuntimeException(String.format("Assignment '%s.%s' is already bound to '%s.%s'.", platformId, assignmentId, platformId, trackId));
            }

            if (!assignment.isAssigned()) {
                //If free, go ahead and add
                apply(new AssignmentAddedToTrack(platformId, assignmentId, trackId, index));
            } else {
                //we already know that it is in this track
                assert assignment.getAssociatedTrack().equals(trackId);
                //and we cans save work if we don't need to move the assignment
                if (track.getAssignments().indexOf(assignmentId) != index) {
                    apply(new AssignmentMovedWithinTrack(platformId, assignmentId, trackId, index));
                } //else NO-OP
            }
        }
    }

    /**
     * Deletes the track
     * @param cmd the command
     */
    @CommandHandler
    public void removeTrack(DeleteTrack cmd) {
        String trackId = cmd.getTrackId();
        TrackEntity track = getTrack(trackId);
        //The command must exist in the first place
        if (track == null) throw new RuntimeException(String.format("Track '%s.%s' does not exist.", platformId, trackId));

        List<String> assignments = new ArrayList<>(track.getAssignments());
        for (String assignmentId : assignments) {
            //Remove all assignments from the track
            apply(new AssignmentRemovedFromTrack(platformId, assignmentId, trackId));
        }
        if (! track.getAssignments().isEmpty()) {
            //Just making sure
            throw new IllegalStateException(String.format("Assignment list should have been empties, but it still contains entries. %s", track.getAssignments().toString()));
        }
        //Finally delete the track itself
        apply(new DeleteTrack(platformId, trackId));
    }

    @EventHandler void on(PlatformCreated event) {
        platformId = event.getPlatformId();
        platformName = event.getPlatformName();
        description = event.getDescription();
    }

    @EventHandler void on(TrackCreated event) {
        tracks.put(event.getTrackId(), new TrackEntity(event.getTrackId(), event.getTrackName(), event.getDescription()));
    }

    @EventHandler void on(TrackDeleted event) {
        tracks.remove(event.getTrackId());
    }

    @EventHandler void on(AssignmentCreated event) {
        assignments.put(event.getAssignmentId(), new AssignmentEntity(event.getAssignmentId(), event.getAssignmentName(), event.getDescription(), event.getSubmissionPoints(), event.getReviewPoints()));
    }



    /**
     * Gets platform id.
     * <p>
     *     The platform id is used to identify the platform in the database and in URLs.
     * </p>
     *
     * @return the platform id
     */
    public String getPlatformId() {
        return platformId;
    }

    /**
     * Gets platform name.
     *
     * @return the platform name
     */
    public String getPlatformName() {
        return platformName;
    }

    /**
     * Gets description.
     *
     * @return the description
     */
    public String getDescription() {
        return description;
    }

    /**
     * Returns a {@link Map} containing all of the {@link TrackEntity tracks} in the platform.
     *
     * @return the tracks
     */
    public Map<String, TrackEntity> getTracks() {
        return tracks;
    }

    /**
     * Gets a particular track.
     *
     * @param trackId the track id
     * @return the track
     */
    public TrackEntity getTrack(String trackId) {
        return tracks.get(trackId);
    }

    /**
     * Determines if the platform contains a track with the specified id.
     *
     * @param trackId the track id
     * @return whether the platform contains the track
     */
    public boolean hasTrack(String trackId) {
        return tracks.containsKey(trackId);
    }

    /**
     * Returns a {@link Map} containing all of the {@link AssignmentEntity assignments} in the platform.
     *
     * @return the assignments
     */
    public Map<String, AssignmentEntity> getAssignments() {
        return assignments;
    }

    /**
     * Gets a particular assignment.
     *
     * @param assignmentId the assignment id
     * @return the assignment
     */
    public AssignmentEntity getAssignment(String assignmentId) {
        return assignments.get(assignmentId);
    }

    /**
     * Determines if the platform contains an assignment with the specified id.
     *
     * @param assignmentId the assignment id
     * @return whether the platform contains the assignment
     */
    public boolean hasAssignment(String assignmentId) {
        return assignments.containsKey(assignmentId);
    }


}
